One discussion that tends to not be had in these types of benchmarks is about memory, resource utilization, and the fact that blocking is a feature.

In a typical multi threaded web application scenario, there's usually two main resources floating around: http thread pools, and database connection pools.

Both are typically designed to not scale up too quickly (e.g. wait a bit before adding a new resource) and block when they hit a configured maximum. This is because it is pretty easy for a single web server these days to become a denial-of-service attack on a database, and it's also pretty easy for a single client (especially as the typical home's Internet bandwidth increases) to become a denial-of-service attack on a web server. Sensible defaults should therefore take into account that a web server should be a good actor in an ecosystem, not that a web server should be a requests-per-second speed demon. When I read benchmarks, I tend to believe that they are essentially testing these defaults, because there is rarely discussion of them. A conservatively tuned database connection pool will block earlier in the load test, but I will respect the conservative defaults of the framework in question.

Memory is also crucially important in the sync-vs-async discussion. Once you decouple request state management from a constrained-sized thread pool, you have to predict how much memory each state will consume. Unless you are doing very little work on each request (and the work will go up as you add features), you will consume your web server's memory with request state far before you exhaust its other resources. In a threaded model, the thread overhead itself takes a lot of memory, so request overhead per thread in a threaded model is higher than an async model. But when you're talking about a web application with increasing numbers of features, this becomes less and less the majority of the request.

Here's a sketch of an example. In the typical thread-pool based model, X requests can happen simultaneously based on the configured maximum of handler threads--if more requests come in than can happen at the same time, they go into a queue. This is nice because unless the web application is very simple, it's far easier to predict the memory overhead of http requests in a queue than active simultaneous requests in the application layer, especially because most web servers put maximums on the size of individual http headers. In other words, the memory cost of each http request in the queue is constant, while the memory-per-request-state in the application layer will grow with the number of features that layer on the application.

Of course none of the above has much to do with asynchronicity vs thread-bounded-ness. You can have a web server with a request queue and then a set of asynchronous handlers that feed off the queue. I do have a weak argument for threads in this scenario. In the constant tug-of-war between server resources and request throughput, monitoring becomes very important. In a thread-based-model, it is easy to use operating-system tools to introspect the threads because the operating system knows what they are. In the asynchronous model the process is a heap of undifferentiated ram. In that scenario you need good tooling to predict how well tuned your request state is relative to resources. In the JVM it's pretty easy to do a heap dump and see how much memory each request is taking, I'm not sure if there's an equally convenient scenario in the Javascript runtime(s) Node.js can utilize, but I'm sure there's a way. What I do know, however, is that complexity increases as you add actors and/or callbacks, because request state is now broken into a chain of independent memory consuming entities, as opposed to a single predictable thread stack and references.

In the midst of all the above I tend to go for flexibility, because the needs are situational. Async when you need it, sync when you need it. With sync being the more predictable model for the above reasons. Async is great for socket servers that are fronting a fixed-sized memory store, like memcached, while sync is more predictable (using today's tools) for a user facing web application with non-fixed per-request costs.

Seriously, though, YMMV. The discerning reader will see that I'm not making a fundamental argument for threads or async, because on one level there is no difference between them. Threads are not coupled to CPU cores, so they themselves are floating request state stack waiting to be scheduled by the OS onto a core. Async request state is a further level of application level decoupling where a pool of requests are waiting to be scheduled onto a thread, which in turn will be scheduled onto a CPU. A second difference has nothing to do with sync vs async but with coupling -- a synchronous model implies utilizing a stack that executes one-at-a-time, while an asynchronous model implies message passing (hence the ability for selective parallelism). Both models have pros and cons when it comes to predictability and resource utilization. The separation between the two will probably end up being a historical artifact.

I'm just sketching some things that I think about when reading all these sync vs async benchmarks, because they're kind of like saying that American cars are slower than German cars because of average speeds on the Autobahn vs the 405 in Los Angeles, without a discussion of the posted speed limits, traffic patterns, etc.