# The Right Way To Use HttpClient In .NET

> The easy way to make HTTP requests in .NET is HttpClient. Unfortunately, it's easy to misuse, and port exhaustion and DNS behavior are some of the most common problems. Here's what you need to know, from IHttpClientFactory to typed clients.

Published: 2023-06-10. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/the-right-way-to-use-httpclient-in-dotnet

The right way to use `HttpClient` in .NET is to avoid creating an instance per request (port exhaustion) and to avoid a naive singleton (stale DNS).
Let `IHttpClientFactory` manage the lifetime for you, or reuse a single instance with a configured `PooledConnectionLifetime`.
This issue covers named clients, typed clients, and when to use each option.

If you're building a **.NET** application, chances are high that you'll need to call an **external API** over **HTTP**.

The easy way to make HTTP requests in .NET is to use the `HttpClient` to send those requests.
And it's a great abstraction to work with, especially with the methods supporting **JSON** payloads and responses.

Unfortunately, it's easy to misuse the `HttpClient`.

**Port exhaustion** and **DNS behavior** are some of the most common problems.

So here's what you need to know about working with `HttpClient`:

- How not to use `HttpClient`
- How to simplify configuration with `IHttpClientFactory`
- How to configure **typed clients**
- Why you should avoid **typed clients** in singleton services
- When to use which option

Let's dive in!

## What Is HttpClient in .NET?

`HttpClient` is the primary class in .NET for sending HTTP requests and receiving HTTP responses from a URI.
It lives in the `System.Net.Http` namespace and is included in the .NET runtime (you don't need extra packages).

You can use it to call REST APIs, download files, or communicate with any HTTP-based service.
It supports all standard HTTP methods (GET, POST, PUT, DELETE, PATCH) and has built-in support for JSON
through extension methods like `GetFromJsonAsync` and `PostAsJsonAsync`.

While `HttpClient` is easy to get started with, it's also easy to misuse.
The most common mistakes are creating too many instances (causing port exhaustion) and holding on to instances for too long (causing stale DNS responses).
The rest of this article covers the right patterns for working with `HttpClient` in .NET.

## The Naive Way To Use HttpClient

The simplest way to work with the `HttpClient` is to just create a new instance, set the required properties and use it to send requests.

What could possibly go wrong?

`HttpClient` instances are meant to be **long-lived**, and reused throughout the lifetime of the application.

Each instance uses its own **connection pool** for isolation purposes, but also to prevent **port exhaustion**.
If a server is under high load, and your application is constantly creating new connections, it could lead to exhausting the available ports.
This will cause an exception at runtime, when trying to send a request.

So how can you avoid this?

```csharp
public class GitHubService
{
    private readonly GitHubSettings _settings;

    public GitHubService(IOptions<GitHubSettings> settings)
    {
        _settings = settings.Value;
    }

    public async Task<GitHubUser?> GetUserAsync(string username)
    {
        using var client = new HttpClient();

        client.DefaultRequestHeaders.Add("Authorization", _settings.GitHubToken);
        client.DefaultRequestHeaders.Add("User-Agent", _settings.UserAgent);
        client.BaseAddress = new Uri("https://api.github.com");

        GitHubUser? user = await client
            .GetFromJsonAsync<GitHubUser>($"users/{username}");

        return user;
    }
}
```

## The Smart Way To Create HttpClient Using IHttpClientFactory

Instead of managing the `HttpClient` lifetime yourself, you can use an **`IHttpClientFactory`** to create the `HttpClient` instance.

Simply call the `CreateClient` method and use the returned `HttpClient` instance to send your HTTP requests.

Why is this a better approach?

The expensive part of the `HttpClient` is the actual message handler - `HttpMessageHandler`.
Each `HttpMessageHandler` has an internal HTTP **connection pool** that can be reused.

The `IHttpClientFactory` will **cache** the `HttpMessageHandler` and reuse it when creating a new `HttpClient` instance.

An important note here is that `HttpClient` instances created by `IHttpClientFactory` are meant to be **short-lived**.

```csharp
public class GitHubService
{
    private readonly GitHubSettings _settings;
    private readonly IHttpClientFactory _factory;

    public GitHubService(
        IOptions<GitHubSettings> settings,
        IHttpClientFactory factory)
    {
        _settings = settings.Value;
        _factory = factory;
    }

    public async Task<GitHubUser?> GetUserAsync(string username)
    {
        using var client = _factory.CreateClient();

        client.DefaultRequestHeaders.Add("Authorization", _settings.GitHubToken);
        client.DefaultRequestHeaders.Add("User-Agent", _settings.UserAgent);
        client.BaseAddress = new Uri("https://api.github.com");

        GitHubUser? user = await client
            .GetFromJsonAsync<GitHubUser>($"users/{username}");

        return user;
    }
}
```

## Reducing Code Duplication With Named Clients

Using `IHttpClientFactory` will solve most of the issues of manually creating an `HttpClient`.
However, we still need to configure the default request parameters every time we obtain a new `HttpClient` from the `CreateClient` method.

You can configure a **named client** by calling the `AddHttpClient` method and passing in the desired name.
The `AddHttpClient` accepts a delegate that you can use to configure the default parameters on the `HttpClient` instance.

```csharp
services.AddHttpClient("github", (serviceProvider, client) =>
{
    var settings = serviceProvider
        .GetRequiredService<IOptions<GitHubSettings>>().Value;

    client.DefaultRequestHeaders.Add("Authorization", settings.GitHubToken);
    client.DefaultRequestHeaders.Add("User-Agent", settings.UserAgent);

    client.BaseAddress = new Uri("https://api.github.com");
});
```

The main difference is you now have to obtain the client by passing the name of the client to `CreateClient`.

But the use of the `HttpClient` looks a lot simpler:

```csharp
public class GitHubService
{
    private readonly IHttpClientFactory _factory;

    public GitHubService(IHttpClientFactory factory)
    {
        _factory = factory;
    }

    public async Task<GitHubUser?> GetUserAsync(string username)
    {
        using var client = _factory.CreateClient("github");

        GitHubUser? user = await client
            .GetFromJsonAsync<GitHubUser>($"users/{username}");

        return user;
    }
}
```

## Replacing Named Clients With Typed Clients

The downside of using **named clients** is having to resolve an `HttpClient` by passing in a name every time.

There's a better way to achieve the same behavior by configuring a **typed client**.
You can do this by calling the `AddClient<TClient>` method and configuring the service that will consume the `HttpClient`.

Under the hood, this is still using a **named client**, where the name is the same as the type name.

And this will also register `GitHubService` with a **transient lifetime**.

```csharp
services.AddHttpClient<GitHubService>((serviceProvider, client) =>
{
    var settings = serviceProvider
        .GetRequiredService<IOptions<GitHubSettings>>().Value;

    client.DefaultRequestHeaders.Add("Authorization", settings.GitHubToken);
    client.DefaultRequestHeaders.Add("User-Agent", settings.UserAgent);

    client.BaseAddress = new Uri("https://api.github.com");
});
```

Inside of `GitHubService`, you inject and use the typed `HttpClient` instance which will have all of the configuration applied.

No more dealing with `IHttpClientFactory` and creating `HttpClient` instances manually.

```csharp
public class GitHubService
{
    private readonly HttpClient client;

    public GitHubService(HttpClient client)
    {
        _client = client;
    }

    public async Task<GitHubUser?> GetUserAsync(string username)
    {
        GitHubUser? user = await client
            .GetFromJsonAsync<GitHubUser>($"users/{username}");

        return user;
    }
}
```

## Why You Should Avoid Typed Clients In Singleton Services

You could run into a **problem** if you inject a **typed client** into a **singleton service**.
Since the **typed client** is **transient**, injecting it in a **singleton service** will cause it to be cached for the lifetime of the **singleton service**.

This will prevent the **typed client** from reacting to DNS changes.

If you want to use a **typed client** in a **singleton service**, the recommened approach is using `SocketsHttpHandler` as the primary handler,
and configuring the `PooledConnectionLifetime`.

Since the `SocketsHttpHandler` will handle connection pooling, you can disable recycling at the `IHttpClientFactory` level by setting `HandlerLifetime` to `Timeout.InfiniteTimeSpan`.

```csharp
services.AddHttpClient<GitHubService>((serviceProvider, client) =>
{
    var settings = serviceProvider
        .GetRequiredService<IOptions<GitHubSettings>>().Value;

    client.DefaultRequestHeaders.Add("Authorization", settings.GitHubToken);
    client.DefaultRequestHeaders.Add("User-Agent", settings.UserAgent);

    client.BaseAddress = new Uri("https://api.github.com");
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
    return new SocketsHttpHandler()
    {
        PooledConnectionLifetime = TimeSpan.FromMinutes(15)
    };
})
.SetHandlerLifetime(Timeout.InfiniteTimeSpan);
```

## SocketsHttpHandler vs HttpClientHandler

Under the hood, every `HttpClient` uses an `HttpMessageHandler` to manage TCP connections.
.NET has two primary handler implementations worth knowing.

**`HttpClientHandler`** is the traditional handler, available since .NET Framework.
On modern .NET runtimes it internally delegates to `SocketsHttpHandler`.
It exposes familiar properties for cookies, automatic decompression, and proxy configuration.

**`SocketsHttpHandler`** is the fully managed, cross-platform handler that debuted in .NET Core 2.1 and is now the default on all current .NET runtimes.
Unlike `HttpClientHandler`, it exposes `PooledConnectionLifetime` and `PooledConnectionIdleTimeout`,
giving you direct control over how long connections stay in the pool before being recycled.
This is exactly what makes `SocketsHttpHandler` valuable when using a typed `HttpClient` in a singleton service, as
you can ensure DNS changes are picked up without relying on `IHttpClientFactory` handler recycling.

For most applications using `IHttpClientFactory`, you don't need to choose explicitly.
Configure `SocketsHttpHandler` directly only when you need fine-grained control over connection-pool behavior.
You wire it up with `ConfigurePrimaryHttpMessageHandler` when registering the client:

```csharp
builder.Services
    .AddHttpClient<MyService>()
    .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
    {
        PooledConnectionLifetime = TimeSpan.FromMinutes(2)
    });
```

## When Should You Use Which Option?

I showed you a few possible options for working with `HttpClient`.

But which one should you use and when?

Microsoft was kind enough to provide us with a set of best practices and [recommended use](https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/http/httpclient-guidelines#recommended-use)
for `HttpClient`.

- Use a `static` or **singleton** `HttpClient` instance with a `PooledConnectionLifetime` configured, since this solves both port exhaustion and tracking DNS changes
- Use `IHttpClientFactory` if you want to move the configuration to one place, but remember that clients are meant to be **short-lived**
- Use a **typed client** if you want the `IHttpClientFactory` configurability

I prefer working with a **typed client**, and I'm mindful of the fact that it's configured as a **transient service**.

Thanks for reading, and have an awesome Saturday.

---

## Frequently asked questions

### What is HttpClient in .NET used for?

HttpClient in .NET is used to send HTTP requests and receive HTTP responses from a URI. It supports GET, POST, PUT, DELETE, and other HTTP methods, and can deserialize JSON responses directly into .NET objects.

### Should HttpClient be static or injected in .NET?

HttpClient should neither be created per-request (causes port exhaustion) nor used as a naive singleton (stale DNS). The recommended approach is to use IHttpClientFactory, which manages lifetime and connection pooling correctly.

### What does AddHttpClient do in ASP.NET Core?

AddHttpClient registers IHttpClientFactory with the DI container and allows you to configure named or typed HttpClient instances with base addresses, headers, retry policies, and other settings.

### Is IHttpClientFactory required in .NET?

IHttpClientFactory is not strictly required, but it is the recommended way to create HttpClient instances in ASP.NET Core applications to avoid port exhaustion and DNS staleness issues.

### What is the difference between HttpClientHandler and SocketsHttpHandler?

SocketsHttpHandler is the default handler on .NET Core 2.1+ and provides better performance and connection pooling. HttpClientHandler is the traditional handler that delegates to the OS-level handler. SocketsHttpHandler gives you more control over connection lifetime via PooledConnectionLifetime.

### Why does HttpClient cause socket exhaustion?

Creating a new HttpClient instance for each request creates a new socket connection that is not immediately released after disposal. Sockets enter a TIME_WAIT state, and if you create many instances quickly, you exhaust the available port range.

### How do you configure a primary HTTP message handler in .NET?

Use ConfigurePrimaryHttpMessageHandler when registering the client: builder.Services.AddHttpClient<MyService>().ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { PooledConnectionLifetime = TimeSpan.FromMinutes(2) }).
