Just show me the code!
As always, if you don’t care about the post I have uploaded the source code on my
Github
.
If you are a .NET veteran, this post is probably not intended for you
.
I’m well aware that there are a ton of great articles (and probably better than this one) on the Internet, explaining exactly how you should properly use
HttpClient
with .NET.
However, the truth is, even with so many resources available, I still come across many cases where its usage is incorrect.
Therefore, I have decided to write a brief post about the most common use scenarios of
HttpClient
.
As I said, I have no intention of writing a theoretical post explaining the ins and outs of how
HttpClient
works. My goal here is to create
a concise post that highlights various common scenarios where
HttpClient
is utilized and discuss the reasons behind its appropriate or inappropriate usage.
netstat command
The
netstat
command is a networking tool that allows us to investigate active network connections on a system, and in this post I will make extensive use of it to monitor
HttpClient
TCP connections.
HttpClient
is used to make HTTP requests to web servers and APIs, and it relies on underlying network connections to perform these tasks. By leveraging the
netstat
command, we can gain insights into the active TCP connections created by
HttpClient
, helping us identify potential issues.
To investigate the active TCP connections that
HttpClient
creates using
netstat
, you can open a command prompt or terminal and enter
netstat -ano
(the ‘-a’ flag shows all connections, the ‘-n’ flag displays IP addresses and port numbers, and the ‘-o’ flag displays the associated process ID (PID) ).
The output will provide a list of all active connections, along with their status, local and remote IP addresses, and associated PID process.
Monitoring HttpClient connections using
netstat
can help you identify if your application is properly closing connections after use or if there are lingering connections that may lead to resource leaks. It can also reveal if there are connection failures, such as connections in a
TIME_WAIT
state, which might indicate issues with connection pooling or DNS resolution.
The next list shows the
netstat
states and their meanings:
ESTABLISHED
: This state indicates that a connection is active and data is being exchanged between the local and remote systems. It signifies a successful connection between the client and server.
TIME_WAIT
: After the connection is closed, it enters the
TIME_WAIT
state. This state ensures that any delayed packets from the previous connection are handled properly. It typically lasts for a few minutes before the connection is fully closed.
CLOSE_WAIT
: This state occurs when the local application has closed the connection, but the remote system has not acknowledged the closure yet. It usually implies that the local application is waiting for the remote system to release the connection.
FIN_WAIT_1
,
FIN_WAIT_2
: These states occur during the process of closing a connection.
FIN_WAIT_1
means the local system has initiated the closure, while
FIN_WAIT_2
indicates the remote system has acknowledged the closure, and the local system is waiting for a final acknowledgment.
LAST_ACK
: This state appears when the local system has initiated the closure, sent a FIN packet, and is waiting for the final acknowledgment from the remote system before the connection is fully closed.
SYN_SENT
: In this state, the local system has sent a synchronization (SYN) packet to initiate a connection with the remote system but has not received a response yet.
SYN_RECEIVED
: The
SYN_RECEIVED
state occurs on the server side when it receives a SYN packet from the client and sends back its
SYN-ACK
packet to acknowledge the connection request.
LISTENING
: When a server application is in the
LISTENING
state, it is waiting and ready to accept incoming connection requests from clients.
CLOSING
: This state occurs when the local system has initiated the closure of the connection, but the remote system is also trying to close the connection simultaneously.
Scenario 1: Create a new HttpClient for every incoming request
Source code
A new
HttpClient
is instantiated every time a new request comes in.
The
HttpClient
is not disposed after being used.
[ApiController]
[Route("[controller]")]
public class ScenarioOneController : ControllerBase
[HttpGet()]
public async Task<ActionResult> Get()
var client = new HttpClient
BaseAddress = new Uri("https://jsonplaceholder.typicode.com/"),
DefaultRequestHeaders = { { "accept", "application/json" } },
Timeout = TimeSpan.FromSeconds(15)
var response = await client.GetAsync(
"posts/1/comments");
if (response.IsSuccessStatusCode)
return Ok(await response.Content.ReadAsStringAsync());
return StatusCode(500);
netstat output
Every time a new request comes in, a new TCP connection is created.
The next video shows how for every request made at the
ScenarioOneController
a new TCP connection is created
TCP connections are not closed after being used, which means that they will linger for some time, awaiting incoming data that will never arrive.
After 2 minutes (default idle timeout) of hanging around doing nothing, the TCP connections will be closed by the operating system and moved to a
TIME_WAIT
state.
The
TIME_WAIT
state is a normal part of the TCP connection termination process, and it occurs after a connection is closed.
During this state, the socket remains in the system for a specific period to ensure that any delayed or out-of-order packets related to the closed connection do not interfere with new connections using the same port.
The duration of the
TIME_WAIT
state can vary depending on the operating system and TCP implementation.
In most modern systems, the
TIME_WAIT
state typically lasts for 30 seconds to 2 minutes.
After some time in the
TIME_WAIT
state, the TCP connection will be terminated, and the socket will be released.
A new
HttpClient
is being created every time a new request comes in, which means that the application has an unnecessary overhead from establishing a new TCP connection for every single request.
If the app is under heavy load this approach can lead to an accumulation of TCP connections on a
ESTABLISHED
state or in a
TIME_WAIT
state, which can cause a port exhaustion problem.
Scenario 2: Create a new HttpClient for every incoming request and dispose of it after use
Source code
A new
HttpClient
is instantiated every time a new request comes in.
The
HttpClient
is disposed right after being used.
[ApiController]
[Route("[controller]")]
public class ScenarioTwoController : ControllerBase
[HttpGet()]
public async Task<ActionResult> Get()
using var client = new HttpClient
BaseAddress = new Uri("https://jsonplaceholder.typicode.com/"),
DefaultRequestHeaders = { { "accept", "application/json" } },
Timeout = TimeSpan.FromSeconds(15)
var response = await client.GetAsync(
"posts/1/comments");
if (response.IsSuccessStatusCode)
return Ok(await response.Content.ReadAsStringAsync());
return StatusCode(500);
netstat output
Every time a new request comes in, a new TCP connection is created.
Similar to scenario 1, TCP connections are not being reused for subsequent requests, but this time, at least the connections are being closed immediately after use.
The fact that the
HttpClient
gets disposed right away (because of the
using
block) causes the TCP connections to move directly to a
TIME_WAIT
state.
During the
TIME_WAIT
state, the socket remains in the system for a specific period to ensure that any delayed or out-of-order packets related to the closed connection do not interfere with new connections using the same port. The
TIME_WAIT
state lasts for 30 seconds to 2 minutes depending on the operating system.
Pros & cons of this scenario
In this scenario, it is less likely for the application to experience port exhaustion issues.
In scenario 1, for each request, the TCP connection would remain in an
ESTABLISHED
state for a few minutes until the operating system forced it to close.
In contrast, in scenario 2, since we are disposing of the HTTP client after its use, the connection is promptly closed, eliminating the period of time during which the connection was lingering in an
ESTABLISHED
state.
A new
HttpClient
is being created every time a new request comes in, which means that the application has an unnecessary overhead from establishing a new TCP connection every single time.
In this scenario, although we have managed to eliminate the fact that TCP connections remain in an
ESTABLISHED
state for a couple of minutes, we are still creating a new TCP connection for each incoming request the controller receives. This situation could still potentially result in issues related to port exhaustion, particularly if the application experiences a high volume of traffic.
Scenario 3: Create a static HttpClient and use it for any incoming requests
Source code
A
static
HttpClient
instance is created once and reused for incoming requests.
[ApiController]
[Route("[controller]")]
public class ScenarioThreeController : ControllerBase
private static readonly HttpClient Client = new()
BaseAddress = new Uri("https://jsonplaceholder.typicode.com/"),
DefaultRequestHeaders = { { "accept", "application/json" } },
Timeout = TimeSpan.FromSeconds(15),
[HttpGet()]
public async Task<ActionResult> Get()
var response = await Client.GetAsync(
"posts/1/comments");
if (response.IsSuccessStatusCode)
return Ok(await response.Content.ReadAsStringAsync());
return StatusCode(500);
netstat output
Now, the TCP connections are being reused.
If the application remains idle for 2 minutes, then the TCP connection will get closed by the operating systen. The next request will force the creation of a new TCP connection.
If a TCP connection is not being used to send a request, it’s considered idle. By default in .NET, an idle TCP connection is closed after 2 minutes.
HttpClient
only resolves DNS entries when a TCP connection is created.
In the current scenario (where we employ a
static
or
singleton
, long-lived
HttpClient
), if the service we are invoking experiences a DNS modification, the established TCP connections will remain oblivious to this change.
In the upcoming video, I modify the hosts file on my computer to redirect the DNS address for
jsonplaceholder.typicode.com
to
127.0.0.1
.
The application should throw an error because there is nothing listening on
127.0.0.1
capable of responding accordingly, but despite this change, the subsequent requests made to
jsonplaceholder.typicode.com
continue responding with a 200 OK status code, that’s because the client remains unaware of the DNS change I made.
TCP connections are being reused, which further reduces the likelihood of experiencing a port exhaustion issue.
If the rate of requests is very high, the operating system limit of available ports might still be exhausted, but the best way to minimize this issue is exactly what we’re doing in this scenario, reusing
HttpClient
instances for as many HTTP requests as possible.
You’ll see a lot of guidelines mentioning this DNS resolution issue when talking about
HttpClient
. The truth is, if your app is making calls to a service where you’re aware that the DNS address won’t change at all, using this approach is perfectly fine.
HttpClient
only resolves DNS entries when a TCP connection is created. If DNS entries changes regularly, then the client won’t notice those updates.
Scenario 4: Create a static or singleton HttpClient with PooledConnectionLifetime and use it for any incoming requests
Source code
A
static
HttpClient
instance is created once and reused for incoming requests.
The
HttpClient
is created using the
PooledConnectionLifetime
attribute. This attribute defines how long connections remain active when pooled. Once this lifetime expires, the connection will no longer be pooled or issued for future requests.
In the next code snippet, the
PooledConnectionLifetime
is set to 10 seconds, which means that TCP connections will cease to be re-issued and be closed after a maximum of 10 seconds. This is highly inefficient and it is only done for demo purposes.
[ApiController]
[Route("[controller]")]
public class ScenarioFourController : ControllerBase
private static readonly HttpClient Client = new(new SocketsHttpHandler
PooledConnectionLifetime = TimeSpan.FromSeconds(10)
BaseAddress = new Uri("https://jsonplaceholder.typicode.com/"),
DefaultRequestHeaders = { { "accept", "application/json" } },
Timeout = TimeSpan.FromSeconds(15),
[HttpGet()]
public async Task<ActionResult> Get()
var response = await Client.GetAsync(
"posts/1/comments");
if (response.IsSuccessStatusCode)
return Ok(await response.Content.ReadAsStringAsync());
return StatusCode(500);
netstat output
TCP connections are being reused.
The
PooledConnectionLifetime
attribute is set to 10 seconds, which means that after 10 seconds the TCP connection will be closed and won’t be reused anymore. The next request will force the creation of a new TCP connection.
Do you remember that in scenario 3, I mentioned an issue with DNS resolution?
DNS resolution only occurs when a TCP connection is created, which means that if the DNS changes after the TCP connection has been created, then the TCP connection is unaware of it.
The solution to avoid this issue is to create
short-lived
TCP connections that can be reused. Thus, when the time specified by the
PooledConnectionLifetime
property is reached, the TCP connection is closed, and a new one is created, forcing DNS resolution to occur again.
You can observe this behavior in the upcoming video.
In the video, I modify the
hosts
file on my computer to redirect the DNS address for
jsonplaceholder.typicode.com
to
127.0.0.1
.
Since there is nothing listening on the
127.0.0.1
address capable of responding to those requests, after 10 seconds (
PooledConnectionLifetime
current value), the HTTP requests start failing with a 500 error. This occurs because the TCP connection has been closed, and a new one has been created, forcing DNS resolution to occur again.
That’s a huge difference from scenario 3, where the requests keep responding with a 200 OK because the DNS resolution never occurred.
Pros & cons of this scenario
TCP connections are being reused, which further reduces the likelihood of experiencing a port exhaustion issue.
It solves the DNS change issue mentioned on scenario 3.
There are no disadvantages in this scenario.
Scenario 4.1: Create a static or singleton HttpClient with PooledConnectionLifetime and PooledConnectionIdleTimeout and use it for any incoming requests
This is scenario 4.1, not scenario 5.
What’s the point of having a scenario 4.1? This scenario is the same as scenario 4 but with a slight modification that I think it is worth mentioning.
Source code
A
static
HttpClient
instance is created once and reused for incoming requests.
The
HttpClient
is created using the
PooledConnectionLifetime
attribute. This attribute defines how long connections remain active when pooled. Once this lifetime expires, the connection will no longer be pooled or issued for future requests.
The
PooledConnectionIdleTimeout
attribute defines how long idle connections remain within the pool while unused. Once this lifetime expires, the idle TCP connection will be closed and removed from the pool.
In the next code snippet, the
PooledConnectionIdleTimeout
is set to 10 seconds, which means that idle TCP connections will be closed after a maximum of 10 seconds. This is highly inefficient and only done for demo purposes.
[ApiController]
[Route("[controller]")]
public class ScenarioFourController : ControllerBase
private static readonly HttpClient Client = new(new SocketsHttpHandler
PooledConnectionLifetime = TimeSpan.FromMinutes(30),
PooledConnectionIdleTimeout = TimeSpan.FromSeconds(10)
BaseAddress = new Uri("https://jsonplaceholder.typicode.com/"),
DefaultRequestHeaders = { { "accept", "application/json" } },
Timeout = TimeSpan.FromSeconds(15),
[HttpGet()]
public async Task<ActionResult> Get()
var response = await Client.GetAsync(
"posts/1/comments");
if (response.IsSuccessStatusCode)
return Ok(await response.Content.ReadAsStringAsync());
return StatusCode(500);
Why is the
PooledConnectionIdleTimeout
attribute worth mentioning? Let’s take a look at the code above.
PooledConnectionLifetime
is set to 30 minutes, which means that TCP connections will be reused during 30 minutes.
PooledConnectionIdleTimeout
is set to 10 seconds, which means that idle TCP connections will be closed after a maximum of 10 seconds,
it doesn’t matter if the
PooledConnectionLifetime
time has been reached or not.
What will happen in a real application?
If the app keeps receiving a constant flow of requests, then the existing TCP connection will be reused. After
30 minutes
, the TCP connection will be closed, and a new TCP connection will be established for the next request.
If for some reason the app doesn’t receive any requests and the TCP connection gets considered as idle, then the TCP connection will be closed after
10 seconds
, it doesn’t matter whether the
PooledConnectionLifetime
time has been reached or not.
Let’s take a look at this behaviour using the
netstat
command:
So, it’s good to know about the
PooledConnectionIdleTimeout
attribute and how it works, as it can disrupt the lifespan of your TCP connections.
Scenario 5: Use IHttpClientFactory
Source code
An
IHttpClientFactory
named client is setup in the
Program.cs
(this Scenario uses an
IHttpClientFactory
named client, you could use a typed client and the behaviour will be exactly the same)
.
The
SetHandlerLifetime
extension method defines the length of time that a
HttpMessageHandler
instance can be reused before being discarded. It works almost identical as the
PooledConnectionLifetime
attribute from the previous scenario.
We use the
CreateClient
method from the
IHttpClientFactory
to obtain a
httpClient
to call our API.
The
SetHandlerLifetime
method is set to 15 seconds, which means that TCP connections will cease to be re-issued and be closed after a maximum of 15 seconds. This is highly inefficient and it is only done for demo purposes.
On
Program.cs
:
builder.Services.AddHttpClient("typicode", c =>
c.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");
c.Timeout = TimeSpan.FromSeconds(15);
c.DefaultRequestHeaders.Add(
"accept", "application/json");
.SetHandlerLifetime(TimeSpan.FromSeconds(15));
On
ScenarioFiveController.cs
:
[ApiController]
[Route("[controller]")]
public class ScenarioFiveController : ControllerBase
private readonly IHttpClientFactory _factory;
public ScenarioFiveController(IHttpClientFactory factory)
_factory = factory;
[HttpGet()]
public async Task<ActionResult> Get()
var client = _factory.CreateClient("typicode");
var response = await client.GetAsync(
"posts/1/comments");
if (response.IsSuccessStatusCode)
return Ok(await response.Content.ReadAsStringAsync());
return StatusCode(500);
netstat output
TCP connections are being reused.
The
SetHandlerLifetime
method is set to 15 seconds, which means that after 15 seconds the TCP connection will be marked for expiration. The next incoming request will spawn a new TCP connection.
In this scenario, unlike Scenario 4 where the TCP connection was closed immediately after the time set by the
PooledConnectionLifetime
attribute had expired, the expiration of a handler will not promptly dispose of the TCP connection. The expired handler will be positioned in a distinct pool, which is periodically processed to dispose of handlers only when they become unreachable.
TCP connections are being reused, which further reduces the likelihood of experiencing a port exhaustion issue.
It solves the DNS change issue mentioned on scenario 3.
It simplifies the declaration and usage of
HttpClient
instances.
The
IHttpClientFactory
keeps everything nice and simple as long as you only need to modify the common
HttpClient
parameters, it might be a bit harder if you need to tweak some of the less common parameters.
The next code snippet is an example of how to set the
PooledConnectionIdleTimeout
attribute discussed on scenario 4.1, as you can see you’ll need to use the
ConfigurePrimaryHttpMessageHandler
extension method and create a new
SocketsHttpHandler
instance, just to set the value of the
PooledConnectionIdleTimeout
attribute.
builder.Services.AddHttpClient("typicode", c =>
c.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");
c.Timeout = TimeSpan.FromSeconds(15);
c.DefaultRequestHeaders.Add(
"accept", "application/json");
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler()
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(5)
.SetHandlerLifetime(TimeSpan.FromMinutes(20));
Up to this point, we have only explored scenarios that affected .NET 5/6/7, but what if we want to use an HttpClient
in an application built with .NET Framework 4.8?
The recommended way to use HttpClient
in .NET Framework 4.8 is using IHttpClientFactory.**
**You can use a static or singleton HttpClient
, if you are certain that you will not encounter DNS changes in the service you are calling.
Can we use a static or singleton HttpClient
with the PooledConnectionLifetime
attribute (like Scenario 4)?
No, we cannot. it doesn’t work with .NET Framework, the SocketsHttpHandler
doesn’t exist in .NET Framework.
HttpClient
is built on top of the pre-existing HttpWebRequest
implementation, you could use the ServicePoint
API to control and manage HTTP connections, including setting a connection lifetime by configuring the ConnectionLeaseTimeout
for an endpoint.
Scenario 6: Using IHttpClientFactory with .NET Framework and Autofac
Source code
This scenario uses Autofac as IoC container.
An IHttpClientFactory
named client is setup in the AutofacWebapiConfig.cs
class.
A few additional steps are required to make IHttpClientFactory
work with Autofac:
Add required packages:
Microsoft.Extensions.Http
IHttpClientFactory
must be registered properly in Autofac IoC container. To do that, we must follow the next steps:
Create a new ServiceCollection
instance.
Add the IHttpClientFactory
named client.
Build the ServiceProvider
and resolve IHttpClientFactory
.
The IHttpClientFactory
must be registered as a Singleton
on Autofac, or it won’t work properly.
AreaRegistration.RegisterAllAreas();
AutofacWebapiConfig.Initialize(GlobalConfiguration.Configuration);
GlobalConfiguration.Configure(WebApiConfig.Register);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
AutofacWebApiConfig
class implementation, looks like this:
public class AutofacWebapiConfig
public static IContainer Container;
public static void Initialize(HttpConfiguration config)
Initialize(config, RegisterServices(new ContainerBuilder()));
public static void Initialize(HttpConfiguration config, IContainer container)
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
private static IContainer RegisterServices(ContainerBuilder builder)
builder.RegisterApiControllers(Assembly.GetExecutingAssembly());
builder.Register(ctx =>
var services = new ServiceCollection();
services.AddHttpClient("typicode", c =>
c.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");
c.Timeout = TimeSpan.FromSeconds(15);
c.DefaultRequestHeaders.Add(
"accept", "application/json");
.SetHandlerLifetime(TimeSpan.FromSeconds(15));
var provider = services.BuildServiceProvider();
return provider.GetRequiredService<IHttpClientFactory>();
}).SingleInstance();
Container = builder.Build();
return Container;
ScenarioSixController.cs
looks like this:
public class ScenarioSixController : ApiController
private readonly IHttpClientFactory _factory;
public ScenarioSixController(IHttpClientFactory factory)
_factory = factory;
public async Task<IHttpActionResult> Get()
var client = _factory.CreateClient("typicode");
var response = await client.GetAsync(
"posts/1/comments");
if (response.IsSuccessStatusCode)
return Ok(await response.Content.ReadAsStringAsync());
return InternalServerError();
Pros & cons of this scenario
TCP connections are being reused, which further reduces the likelihood of experiencing a port exhaustion issue.
It solves the DNS change issues mentioned on scenario 3.
To avoid creating a new TCP connection every time a new request comes in, it is crucial to register the
IHttpClientFactory
as a Singleton in Autofac.