C# – Configuring HttpClient connection keep-alive

When you use a single instance of HttpClient to send requests, it keeps connections open in order to speed up future requests. By default, idle connections are closed after 2 minutes, and otherwise will be kept open forever (in theory). In reality, the connection can be closed by the server-side (or other external factors) regardless of the client-side settings.

There are two settings that control how long a connection will be kept alive. You can change them. They’re different in .NET Framework and .NET Core. Here is a table showing the settings, their defaults, and the properties you can set to change them:

SettingDefault.NET Framework.NET Core
Idle connection timeout2 minsServicePoint.MaxIdleTimeSocketsHttpHandler.PooledIdleConnectionTimeout
Max connection lifetimeForeverServicePoint.ConnectionLeaseTimeoutSocketsHttpHandler.PooledConnectionLifetime

In this article, I’ll show examples of changing these settings in .NET Framework and .NET Core, how to close a connection right away, and discuss the server-side configuration.

Note: SocketsHttpHandler was introduced in .NET Core 2.1.

Monitoring the connections

You can use netstat to monitor the connection to see the effects of changing the settings. Instead of showing the netstat results in every section, I’ll simply discuss the high-level end results.

I’m running a web API locally on port 9000, and making connections in a console app running locally. Here’s what netstat looks like when a connection is opened:

C:\WINDOWS\system32>netstat -an | find "9000"
  TCP    127.0.0.1:9000         0.0.0.0:0              LISTENING
  TCP    [::1]:2867             [::1]:9000             ESTABLISHED
  TCP    [::1]:9000             [::]:0                 LISTENING
  TCP    [::1]:9000             [::1]:2867             ESTABLISHED
Code language: plaintext (plaintext)

Changing the idle connection timeout

By default, an idle connection is closed after 2 minutes. If a connection is not currently being used to send a request, it’s considered idle. In the examples below, I’ll change the idle connection timeout to 5 minutes.

In .NET Framework

Set ServicePoint.MaxIdleTime to change the idle connection timeout:

//create the single instance
httpClient = new HttpClient();

var sp = ServicePointManager.FindServicePoint(new Uri("https://localhost:9000"));
sp.MaxIdleTime = (int)TimeSpan.FromMinutes(5).TotalMilliseconds;
Code language: C# (cs)

Note: You can set this at the ServicePointManager level if you want to apply it to all URLs.

In .NET Core

Set SocketsHttpHandler.PooledConnectionIdleTimeout and pass the handler to the HttpClient:

var socketsHttpHandler = new SocketsHttpHandler()
{
	PooledConnectionIdleTimeout = TimeSpan.FromMinutes(5),
};
httpClient = new HttpClient(socketsHttpHandler);
Code language: C# (cs)

Results

Here’s the code for sending a request:

var response = await httpClient.GetAsync("https://localhost:9000/stocks/MSFT");
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
Code language: C# (cs)

When a request is sent, a connection is opened. After the request is done, the connection is idle.

The idle connection timeout is set to 5 minutes, so there are two possible outcomes:

  • Another request is sent before 5 minutes. The connection is still open, so it will be reused. The idle timer will be reset.
  • No request is sent, so the connection remains idle for 5 minutes and is closed.

As you can see, a connection can be held open forever because the idle timer is reset every time the connection is reused.

Change the max connection lifetime

By default, connections can stick around forever as long as they’re being used. If this is undesirable, you can change it. In the examples below, I’ll limit the connection lifetime to 5 minutes. The results are different between .NET Framework and .NET Core.

In .NET Framework

Set ServicePoint.ConnectionLeaseTimeout to change the max connection lifetime:

//create the single instance
httpClient = new HttpClient();

var sp = ServicePointManager.FindServicePoint(new Uri("https://localhost:9000"));
sp.ConnectionLeaseTimeout = (int)TimeSpan.FromMinutes(5).TotalMilliseconds;
Code language: C# (cs)

This will close the connection after 5 minutes no matter how long it’s been idle. It will even close lingering half-closed connections.

Unlike in .NET Core, when this closes a connection, it doesn’t leave it in a half-closed state.

In .NET Core

Set SocketsHttpHandler.PooledConnectionLifetime to change the max connection lifetime:

var socketHttpHandler = new SocketsHttpHandler()
{
	PooledConnectionLifetime = TimeSpan.FromMinutes(5),
};
httpClient = new HttpClient(socketHttpHandler);
Code language: C# (cs)

I noticed two things when using this:

  • It’s not precise. It always seems to close the connection ~30 seconds after the time specified. Ex: If I specify 1 minute, it will actually close the connection after 1.5 minutes. Note: It may be polling internally with a hardcoded interval.
  • It closes the connection in the same way that idle connections are closed. It leaves the connection in a half-closed state, where it lingers around for a bit. Note: This is unlike the behavior in .NET Framework, where the connections are obliterated.

Results

A good way to see the effects of setting the max connection lifetime is to send connections periodically:

while (true)
{
	var response = await httpClient.GetAsync("https://localhost:9000/stocks/MSFT");
	response.EnsureSuccessStatusCode();
	Console.WriteLine(await response.Content.ReadAsStringAsync());

	await Task.Delay(TimeSpan.FromMinutes(1));
}
Code language: C# (cs)

This is sending requests every 1 minute. Upon sending the first request, a connection is opened. This means the connection is never idle for more than 1 minute.

Since the max connection lifetime is set to 5 minutes, the first connection is closed after 5 minutes, and a new connection is opened for the subsequent requests.

This shows that setting the max connection lifetime caps how long a connection will stick around no matter how much it’s being used.

Close the HttpClient connection right away

HTTP connections are persistent by default (since HTTP 1.1) to allow for reuse. Since it’s the default, you don’t have to do anything special to enable this behavior. If you want to close the connection right away instead, add the Connection: close request header.

The simplest way to do this with HttpClient is by setting DefaultRequestHeaders.ConnectionClose = true.

httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.ConnectionClose = true;
Code language: C# (cs)

This will add the Connection: close header to all requests sent with this HttpClient instance. Every request will open a connection and close it when done. Be careful with this.

This works the same in .NET Framework and .NET Core.

Server-side configuration

Idle connections can be closed by the server (or any external factor) regardless of the client-side settings. The setting that controls this is usually referred to as keep-alive timeout (this is not the same as TCP KeepAlive). This controls how long an idle connection will be kept open.

The default keep-alive timeout varies between different web servers. For example:

  • In Apache HTTP Server 2.4, the KeepAliveTimeout setting defaults to 5 seconds.
  • In ASP.NET Core Kestrel, the Limits.KeepAliveTimeout setting defaults to 2 minutes.
  • In nginx, the keepalive_timeout setting defaults to 75 seconds.

Both the client-side and server-side settings affect how long an idle connection will be held open. Whichever side has the lower value will determine when the idle connection is closed. For example, if you have a client-side idle connection timeout of 5 minutes, and it’s 2 minutes on the server-side, then an idle connection will be closed by the server after 2 minutes.

ASP.NET Core Kestrel

Here’s an example of how to change the keep-alive timeout value in ASP.NET Core when using Kestrel:

var builder = Host.CreateDefaultBuilder(args)
	.ConfigureWebHostDefaults(webBuilder =>
	{
		webBuilder.UseKestrel(options => 
		{
			options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(5);
		})
		.UseStartup<Startup>()
		.UseUrls(url)
	});
Code language: C# (cs)