Why are IIS threads so precious as compared to regular CLR threads?

I'm reading about AsyncControllers in ASP.NET MVC.

It seems that the sole reason why they exist is so that the IIS threads can be saved while the long running work is delegated to regular CLR threads, that seem to be cheaper.

I have a couple of questions here:

  • Why are these IIS threads so expensive to justify this whole architecture built to support asynchronous controllers?
  • How do I know/configure how many IIS threads are running in my IIS application pool?

ASP.NET processes requests by using threads from the .NET thread pool. The thread pool maintains a pool of threads that have already incurred the thread initialization costs. Therefore, these threads are easy to reuse. The .NET thread pool is also self-tuning. It monitors CPU and other resource utilization, and it adds new threads or trims the thread pool size as needed. You should generally avoid creating threads manually to perform work. Instead, use threads from the thread pool. At the same time, it is important to ensure that your application does not perform lengthy blocking operations that could quickly lead to thread pool starvation and rejected HTTP requests.

Disk I/O, web service calls, are all blocking. There are best optimized by using async calls. When you make an async call, asp.net frees your thread and the request will be assigned to another thread when the callback function is invoked.

To configure the number of threads you can set:

<system.web>
    <applicationPool maxConcurrentRequestsPerCPU="50" maxConcurrentThreadsPerCPU="0" requestQueueLimit="5000"/>
</system.web>

Refer: ASP.NET Thread Usage on IIS 7.5, IIS 7.0, and IIS 6.0

These are the setting that Microsoft best practices recommend:

  • Set maxconnection to 12 * # of CPUs. This setting controls the maximum number of outgoing HTTP connections that you can initiate from a client. In this case, ASP.NET is the client. Set maxconnection to 12 * # of CPUs.
  • Set maxIoThreads to 100. This setting controls the maximum number of I/O threads in the .NET thread pool. This number is automatically multiplied by the number of available CPUs. Set maxloThreads to 100.
  • Set maxWorkerThreads to 100. This setting controls the maximum number of worker threads in the thread pool. This number is then automatically multiplied by the number of available CPUs. Set maxWorkerThreads to 100.
  • Set minFreeThreads to 88 * # of CPUs. This setting is used by the worker process to queue all the incoming requests if the number of available threads in the thread pool falls below the value for this setting. This setting effectively limits the number of requests that can run concurrently to maxWorkerThreads - minFreeThreads. Set minFreeThreads to 88 * # of CPUs. This limits the number of concurrent requests to 12 (assuming maxWorkerThreads is 100).
  • Set minLocalRequestFreeThreads to 76 * # of CPUs. This setting is used by the worker process to queue requests from localhost (where a Web application sends requests to a local Web service) if the number of available threads in the thread pool falls below this number. This setting is similar to minFreeThreads but it only applies to localhost requests from the local computer. Set minLocalRequestFreeThreads to 76 * # of CPUs.

Note: The recommendations that are provided in this section are not rules. They are a starting point.

You would have to benchmark your application to find what works best for your application.


IIS threads are taken from the default thread pool, which is limited by default based on number of processor cores. If this thread pool queue becomes backed up, IIS will stop responding to requests. By using async code, the thread pool thread can be returned to the pool while the async operation takes place, allowing IIS to service more requests overall.

On the other hand, spawning a new thread on your own does not utilize a thread pool thread. Spawning an unchecked number of independent threads can also be a problem, so it's not a cure all fix to the IIS thread pool issue. Async IO is generally preferred either way.

As for changing the number of threads in the thread pool, check here. However, you should probably really avoid doing so.


Our Webservice need from time to time to serve 100 requets/second while the rest of the time it's 1 request/second. Analazyng IIS logs we found out it tooks around 28s when burst occurs to serve such calls.

Applying Microsoft best practices as cited by @nunespascal drasticly reduced time to 1s in our case.

Below is a the Powershell script we currently use when we deploy our production servers. It updates machine.config taking count of logical Processor number.

<# Get and backup current machine.config #>
$path = "C:\Windows\Microsoft.Net\Framework\v4.0.30319\Config\machine.config";
$xml = [xml] (get-content($path));
$xml.Save($path + "-" + (Get-Date -Format "yyyyMMdd-HHmm" ) + ".bak");

<# Get number of physical CPU #>
$physicalCPUs = ([ARRAY](Get-WmiObject Win32_Processor)).Count;

<# Get number of logical processors #>
$logicalProcessors = (([ARRAY](Get-WmiObject Win32_Processor))[0] | Select-Object “numberOfLogicalProcessors").numberOfLogicalProcessors * $physicalCPUs;

<# Set Number of connection in system.net/connectionManagement #>
$systemNet =  $xml.configuration["system.net"];
if (-not $systemNet){
    $systemNet = $xml.configuration.AppendChild($xml.CreateElement("system.net"));
}

$connectionManagement = $systemNet.connectionManagement;
if (-not $connectionManagement){

    $connectionManagement = $systemNet.AppendChild($xml.CreateElement("connectionManagement"));
}

$add = $connectionManagement.add;
if(-not $add){
    $add = $connectionManagement.AppendChild($xml.CreateElement("add")) ;
}
$add.SetAttribute("address","*");
$add.SetAttribute("maxconnection", [string]($logicalProcessors * 12) );

<# Set several thread settings in system.web/processModel #>
$systemWeb =  $xml.configuration["system.web"];
if (-not $systemWeb){
    $systemWeb = $xml.configuration.AppendChild($xml.CreateElement("system.web"));
}

$processModel = $systemWeb.processModel;
if (-not $processModel){
    $processModel = $systemWeb.AppendChild($xml.CreateElement("processModel"));
}
$processModel.SetAttribute("autoConfig","true");
$processModel.SetAttribute("maxWorkerThreads","100");
$processModel.SetAttribute("maxIoThreads","100");
$processModel.SetAttribute("minWorkerThreads","50");
$processModel.SetAttribute("minIoThreads","50");

<# Set other thread settings in system.web/httRuntime #>
$httpRuntime = $systemWeb.httpRuntime;
if(-not $httpRuntime){
    $httpRuntime = $systemWeb.AppendChild($xml.CreateElement("httpRuntime"));
}
$httpRuntime.SetAttribute("minFreeThreads",[string]($logicalProcessors * 88));
$httpRuntime.SetAttribute("minLocalRequestFreeThreads",[string]($logicalProcessors * 76));

<#Save modified machine.config#>
$xml.Save($path);

This solution came to us from a blog article witten by Stuart Brierley back in 2009. We sucessfully tested it with Windows Server from 2008 R2 to 2016.


Actually what is written in article you have linked is not true. Async pattern isn't there to free "super costly IIS Worker Threads" and use in background some other "cheap threads".

Async pattern is there simply to free threads. You can benefit from it in scenarios where you do not need your threads (and best even not your local machine).

I can name two example scenarios (both I/O):

First:

  1. BeginRequest
  2. Begin async file read
  3. during file read you don't need your thread - so other requests can use it.
  4. File read ends - you get thread from app pool.
  5. Request finishes.

And almost identical second:

  1. BeginRequest
  2. Begin async call to WCF service.
  3. we can leave our machine and don't need our thread - so other requests can use it.
  4. We get response from remote service - we get some thread from app pool to continue.
  5. Request finishes.

It's usually safe to read msdn. You can get information about async pattern here.