runtime: HttpClient Performance Slow Compared to .NET 4.7

I have conducted a basic performance test of HttpClient in .NET Core 2.0 versus HttpClient in .NET Framework 4.7 and have noticed a gap in performance. The .NET Framework 4.7 HttpClient/WebClient outperforms .NET Core’s HttpClient by ~1.5-2x in terms of how long it takes to complete a batch of n requests.

Testing

The test is a console app (run in release mode on Windows 7 16GB 3.5GHz) with identical code for .NET Core/Framework that follows this sequence:

  1. Create a single, shared HttpClient instance with a maximum of n (for testing n=10,100) connections.
// .NET Core 2.0
var httpClient = new HttpClient(new HttpClientHandler { MaxConnectionsPerServer = 100 });

// .NET Framework 4.7
ServicePointManager.DefaultConnectionLimit = 100;
var httpClient = new HttpClient();

// .NET Framework 4.7 - WebClient instance is created ONCE PER REQUEST
ServicePointManager.DefaultConnectionLimit = 100;
var webClient = new WebClient();
  1. Start 10,000 simulataneous requests and time how long each request takes to complete + how long all take to complete. Here is code demonstrating how requests are made / timed.
private void RunTest(int count)
{
    // requests is a list of Request data structures used to store individual request latency / responses
    var requests = Enumerable.Range(0, count).Select(j => CreateRequest(j)).ToList();
    
    // this stopwatch is for duration of all requests
    var stopwatch = Stopwatch.StartNew();
    
    // start all requests and wait for completion
    var requestTasks = requests.Select(MakeRequest).ToArray();
    Task.WaitAll(requestTasks);
    
    stopwatch.Stop();
    // total run time = stopwatch.ElapsedMilliseconds
}

private Task MakeRequest(Request request)
{
    var stopwatch = Stopwatch.StartNew();
    var response = await httpClient.GetStringAsync(request.Url);
    stopwatch.Stop();
    
    // save request duration and response
    request.DurationMs = stopwatch.ElapsedMilliseconds;
    request.ResponseId = ParseResponse(response);
}

I am testing against a basic python server that is well under capacity to rule out any server side bottlenecks. The server returns a simple JSON response containing an id field which is used to validate the HttpClient responses.

{ "id": "some_identifier" }

JSON deserialization / response validation is NOT included in performance stats.

Results

These are average statistics (in milliseconds) of 5 separate runs of 10,000 requests each. It is worth mentioning that the actual time spent on each request was much less with .NET Core’s HttpClient, it’s just that the batch of requests takes longer to complete as a whole vs .NET Framework.

framework total run time (ms) avg req time (ms) median req time (ms) total time spent on requests (s)
.NET Core 2.0 5614 216 282 777
.NET 4.7 3055 1355 1339 10,585

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 6
  • Comments: 35 (12 by maintainers)

Most upvoted comments

It can’t come too soon, in my experience so far, .net core performance is unacceptable for production use, compared even to .net framework, Let’s not bother comparing it to Node, Java, Python, etc, where it gets it’s ars soundly handed to itself repeatedly.

We just tried and we can confirm that .NET Core 2.1 Preview 1 is way more performant than 2.0 under linux using docker containers, it now takes around 90ms for HttpClient to call our microservices, where it tooks ~400ms using 2.0.5.

Congrats, we are now eagerly waiting for the 2.1 GA version. 😃

Refit uses HttpClient under the hood so this might be related

It’s possible it’s using it “incorrectly”. If it’s creating and disposing a new HttpClient() instance on every access, that is going to be more expensive on .NET Core. In .NET Framework, HttpClient is a thin wrapper around HttpWebRequest, with the connection pool managed separately, so disposing of the HttpClient instance won’t tear down connections in the connection pool. In .NET Core, the handler owned by HttpClient owns the connection pool, and if you do new HttpClient(), it’s creating a new handler that it owns. It’s possible the slow down you’re seeing is because, due to the way HttpClient is being used, it’s creating a new connection on every access in .NET Core and it’s not in .NET Framework.

@BKlippel Can you share the data you’re basing that on? Have you tried using .Net Core 2.1 RC1 to see if it’s an improvement?

As someone who lives in .net land both at work and at home, if you’re getting generally bad performance with .net, its your programming skills, not the platform.

I agree that the skill is indeed one aspect. But Clr performance can’t be ignored.

A friend of mine submitted his project BeetleX to TechEmpower . the result No.23 is not good enough (A far cry from the No.1 – actix a rust project).

Means nothing. I’d like to see the results of a performance profiler hooked directly up to tests against the project in question. As is, the stuff you’ve linked means nothing. Unless you’re going to submit a PR or learn the basics of programming, please stop making noise on repositories that thousands of people are following.

@sgf, you’ve been going around commenting on a variety of issues, making broad negative claims about performance, with nothing to substantiate those claims. If you’re hitting a performance issue, please open a new issue that includes a repro of the problem and specifics about what the exact problem is.

@soualid you can try 2.1 right now. It has the new networking stack mentioned above, which is faster, esp. on Linux.

  • Try .NET Core 2.1 Preview1 - it is opt-in there (see details):
    • Environment variable: COMPlus_UseManagedHttpClientHandler=true
    • AppContext: System.Net.Http.UseManagedHttpClientHandler=true
  • Or try our recent daily builds - it is default networking stack there.

Ohhh snap got called out sup dog