async vs. sync benchmark

Asynchronous Performance

Karol Rossa
4 min readJun 7, 2022

In a previous article, I have described how async works. Today I would like to talk about async performance compared to synchronous calls in .NET 6 Web API on the Kestrel server. Here you can check the difference in memory allocation.

.NET 6 allows me to prepare necessary code in just a couple of lines. I will use a simple tool for measuring performance, the apache benchmark.

Code Repository

https://github.com/krossa/AsyncBenchmark

Code exposes two endpoints and adds to each request a 2-second delay. Deley represents calls to external resources like the database. I have also added a background thread to monitor ThreadPool activity, which I will explain further in the article.

  • (sync) — An old-fashioned call. A request is delayed by Thread.Sleep, which blocks the thread.
  • (async) — Not so new guy on the block. This time Task.Delay imitates an asynchronous call, which does not block the thread.

Let’s take a look at the apache benchmark command:

ab -n 100 -c 100 -k -s 120 “http://localhost:5000/async"

  • c — Number of concurrent clients working simultaneously.
  • n — Number of requests to perform for the benchmarking session.
  • k — Enable the HTTP KeepAlive feature, i.e., perform multiple requests within one HTTP session. Default is no KeepAlive.
  • s — Maximum number of seconds to wait before the socket times out.

Comparison

I have performed tests for different numbers of concurrent clients to compare results with other workloads. After an initial test on 100 clients, I had to add a background thread to display current thread activity. The reason for that was too similar performance between async and sync requests. I know Kestrel is not the best performance server, but we will be able to see trends.

100 concurrent clients

100 clients statistics
100 clients statistics
100 clients graphs
100 clients graphs

With a low number of concurrent requests, the server can handle async and sync requests similarly. Because of that, I had added a background thread that monitors thread activity and checks status every second. Thread switching is performed so fast for controller asynchronous action that it almost looks like no threads are being used. This will be the same even for a more significant number of concurrent clients. For 100 clients, this was the biggest difference for synchronous action. At peak workload, the sync method had to block 100 threads. Additionally, async calls quickly reached maximum performance at 50 requests per second and maintained it.

1 000 concurrent clients

1 000 clients statistics
1 000 clients statistics
1 000 clients graphs
1 000 clients graphs

With 1000 concurrent clients, async has already outperformed sync calls in every possible way. Sync has performed significantly worse and had to block up to 450 threads during peak workload. For async calls, we can observe that graphs have a similar shape to 100 clients. Total time grows linearly, and a maximum number of 500 requests per second was again obtained quickly and maintained.

10 000 concurrent clients

10 000 clients statistics
10 000 clients statistics
10 000 clients graphs
10 000 clients graphs

For 10 000 concurrent clients, we are missing statistics for a synchronous controller action. This is because the Kestrel server could not handle this workload. For async calls, graphs again have a similar shape, and the same goes for a number of blocked threads, basically none.

Conclusions

As expected, async outperforms sync in every possible way. Numbers don’t lie :) I can imagine that a more powerful server could handle more synchronous requests simultaneously. But at the same time, we can see that Kestrel could process workload for async calls without sweat. We can draw one more conclusion by looking at the startup process for sync and async calls.

sync startup
sync startup
async startup
async startup

The server needs to ‘warm up’ before handling sync calls at maximum capacity. On the other hand, it can process async calls at full capacity from the beginning.

We all have expected where this is going before reading this article. Use async instead of sync :)

--

--