Dissection of an ASP.NET 2.0 request processing flow

For my first technical post, I wanted to discuss about IIS6 & ASP.NET request processing, and their related queuing & threading mechanisms
I see quite every day some people who misunderstand the processing flow in their production applications, whereas it helps a lot to diagnose potential contentions.
For sure I’ve been here only using public symbols store (http://msdl.microsoft.com/download/symbols) and public information for the explanation bellow.

Quick reminds around IIS6 architecture

As all of you know, IIS6 has been completely re-designed compared to previous versions, and one of the biggest change is the usage of a kernel HTTP dedicated driver, http.sys
This allowed to move all the HTTP processing and part of the caching mechanisms to the kernel, that improves a lot performances, compared to the old IIS5 usermode & winsock architecture.

Take a look at
Web Application Infrastructure – Performance and scability (that following graph is coming from) for some detailed explanations about those architectural changes.
It shows that http.sys now handles a dedicated request queue for each application pool, and those worker processes pick them up.

IIS6 Kernel Queues 

Speaking about user mode side, we have still inetinfo.exe that is in charge of metabase management & other services like FTP and SMTP.
IIS Application Pools are handled by one (or more for webgardens) worker process named w3wp.exe, that will be the hosting your server code.
Following graph comes from
Version Differences in IIS Web Application Features article and details this new architecture:

IIS6 WPI mode


IIS Request processing & queues

That being reminded, so what are the queuing mechanisms in place, and how can I locate where my site may have a performance bottleneck?

Let’s follow an ASPX request processing, from kernel mode until the normal page lifecycle.

1.     Inside http.sys (aka kernel side)

As explained above, each client HTTP request reaches first our kernel driver http.sys.
I won’t go in details about what it does exactly, but here are the first steps, quoting IIS Request Processing:

“A request arrives at HTTP.sys:

1)     HTTP.sys determines if the request is valid. If the request is not valid, it sends a code for an invalid request back to the client.

2)     If the request is valid, HTTP.sys checks to see if the request is for static content (HTML) because static content can be served immediately.

3)     If the request is for dynamic content, HTTP.sys checks to see if the response is located in its kernel-mode cache.

4)     If the response is in the cache, HTTP.sys returns the response immediately.

5)     If the response is not cached, HTTP.sys determines the correct request queue, and places the request in that queue.

6)     If the queue has no worker processes assigned to it, HTTP.sys signals the WWW service to start one.”


So at that time the client request is sent into a kernel mode queue (per application pool), to be picked up by the corresponding IIS worker process.
That is the first queue that will be in use while processing our request, and you can configure its max size in IIS MMC – Application Pool – “Limit the kernel request queue to”.
If one of the kernel queue gets exhausted (exceed the configured limit) then http.sys will return an
HTTP 503 QueueFull error within httperr log files.

That means the user mode application pool doesn’t pick up requests quick enough, so more requests are sitting in kernel than the max allowed.
For sure you won’t be able from a user dump to check current utilization of a kernel specific application pool’s queue

2.     Inside W3 Thread Pool 

With IIS6, userland processing begins here, when w3wp.exe picks up requests to handle from this kernel queue.
Remember on IIS4 & IIS5, all requests were first going to inetinfo.exe, that was redirecting them either via COM to IWAM interface or to ASPNET async named pipe.

To pick up those requests, w3wp.exe just handles a dedicated thread pool bound to an I/O Completion Port.
That thread pool is implemented within w3tp.dll, and so is always used, regardless of the type of requests it’s getting.
Indeed W3 thread pool (w3tp.dll) only grabs completion packets, so it hasn’t to know now if that async I/O completion is a request or not.

The completion routine that will get now executed within w3dt.dll, that is the interface between IIS Worker Process & Http.sys.
Now those completion packets will be turned into native requests, that are what the useful iisinfo!clientconns command will show you (windbg extension brought with IIS Debug Diagnostic Tools).


0:005> !iisinfo.clientconns

UL_NATIVE_REQUEST listhead at: 0x5a36a064



Item# 6, UL_NATIVE_REQUEST = 0x00d5bbc0


Client IP/Port:

Server IP/Port:

Host Header:           localhost:80

Request state:         NREQ_STATE_PROCESS

Requested URL:         GET /wicked/DummyPage.aspx HTTP/1.1


Starttime (0x0b537877) Uptime (0x0bc44db8)

Request active 7394625 ms (0 days: 02:03:14.625)


Once we have native requests, IIS has to decide what handler will execute it, depending on the corresponding scriptmap configuration (ISAPI extension, CGI handler, etc…).
In the case of an ASPX page, for sure our handler is the ISAPI extension aspnet_isapi.dll.


0:009> kL999

ChildEBP RetAddr 

00d4fe04 5a322991 aspnet_isapi!HttpExtensionProc

00d4fe24 5a3968ff w3isapi!ProcessIsapiRequest+0x214

00d4fe58 5a3967e0 w3core!W3_ISAPI_HANDLER::IsapiDoWork+0x3fd

00d4fe78 5a396764 w3core!W3_ISAPI_HANDLER::DoWork+0xb0

00d4fe98 5a3966f4 w3core!W3_HANDLER::MainDoWork+0x16e

00d4fea8 5a3966ae w3core!W3_CONTEXT::ExecuteCurrentHandler+0x53

00d4fec4 5a396648 w3core!W3_CONTEXT::ExecuteHandler+0x51

00d4feec 5a392264 w3core!W3_STATE_HANDLE_REQUEST::DoWork+0x9a

00d4ff10 5a3965ea w3core!W3_MAIN_CONTEXT::DoWork+0xa6

00d4ff2c 5a36169f w3core!W3_MAIN_CONTEXT::OnNewRequest+0x55

00d4ff38 5a361650 w3dt!UL_NATIVE_REQUEST::DoStateProcess+0x48

00d4ff48 5a3616ca w3dt!UL_NATIVE_REQUEST::DoWork+0x7f

00d4ff5c 5a3024de w3dt!OverlappedCompletionRoutine+0x1a

00d4ff8c 5a3026bc W3TP!THREAD_POOL_DATA::ThreadPoolThread+0x73

00d4ffa0 5a301db9 W3TP!THREAD_POOL_DATA::ThreadPoolThread+0x24

00d4ffb8 77e6608b W3TP!THREAD_MANAGER::ThreadManagerThread+0x39

00d4ffec 00000000 kernel32!BaseThreadStart+0x34


Maybe you noticed we have now a new module called webengine.dll in addition to aspnet_isapi.dll.
With ASP.NET 2.0, aspnet_isapi.dll became a very light ISAPI extension, that just calls into webengine.dll, in order to create AppDomain and to drive the request processing.
Webengine.dll gives all its power on IIS7, where it is loaded as a global module, and provides the managed code extensibility inside IIS7 Integrated Request Pipeline,  but that’s another story.

3.     Inside ASP.NET ISAPI handler

So we are now at the point that webengine.dll gets the .NET request to execute from the ECB it gets (the well known ISAPI “Extension Control Block”).


0:009> kp

ChildEBP RetAddr 

00d4fe04 5a322991 webengine!AspNetHttpExtensionProc

00d4fe24 5a3968ff w3isapi!ProcessIsapiRequest+0x214

00d4fe58 5a3967e0 w3core!W3_ISAPI_HANDLER::IsapiDoWork+0x3fd

00d4fe78 5a396764 w3core!W3_ISAPI_HANDLER::DoWork+0xb0

00d4fe98 5a3966f4 w3core!W3_HANDLER::MainDoWork+0x16e

00d4fea8 5a3966ae w3core!W3_CONTEXT::ExecuteCurrentHandler+0x53

00d4fec4 5a396648 w3core!W3_CONTEXT::ExecuteHandler+0x51

00d4feec 5a392264 w3core!W3_STATE_HANDLE_REQUEST::DoWork+0x9a

00d4ff10 5a3965ea w3core!W3_MAIN_CONTEXT::DoWork+0xa6

00d4ff2c 5a36169f w3core!W3_MAIN_CONTEXT::OnNewRequest+0x55

00d4ff38 5a361650 w3dt!UL_NATIVE_REQUEST::DoStateProcess+0x48

00d4ff48 5a3616ca w3dt!UL_NATIVE_REQUEST::DoWork+0x7f

00d4ff5c 5a3024de w3dt!OverlappedCompletionRoutine+0x1a

00d4ff8c 5a3026bc W3TP!THREAD_POOL_DATA::ThreadPoolThread+0x73

00d4ffa0 5a301db9 W3TP!THREAD_POOL_DATA::ThreadPoolThread+0x24

00d4ffb8 77e6608b W3TP!THREAD_MANAGER::ThreadManagerThread+0x39

00d4ffec 00000000 kernel32!BaseThreadStart+0x34


The first thing webengine.dll will do (same for aspnet_isapi on 1.1) will be to check if its application queue is maxed out, otherwise it will send back a HTTP 503 – Server Too Busy error.
So here is our 2nd queue inside IIS processing, called “Application Queue”, that will contain .NET requests, called HttpCompletion instances.
We can configure the size of this queue using appRequestQueueLimit attribute of httpRuntime element of your web/machine.config (the default is 5000 on 2.0 but was only 100 on 1.0/1.1).


To check from a dump file the current status of that application queue, you can compare g_RequestQueueLimit with HttpCompletion::s_RequestsCurrent (for .NET 1.1 they are hosted in aspnet_isapi.dll)
For example bellow only 36 requests are processed and application queue is configured to handle maximum of 5000 requests (default setting).


0:029> ?poi(webengine!HttpCompletion::s_RequestsCurrent)

Evaluate expression: 36 = 00000024


0:028> ?poi(webengine!g_RequestQueueLimit)

Evaluate expression: 5000 = 00001388


s_RequestsCurrent is only used to check the state of that application queue, after that if you want to know how much ongoing completion requests you have, check s_ActiveManagedRequestCount


0:029> ?poi(webengine!HttpCompletion::s_ActiveManagedRequestCount)

Evaluate expression: 36 = 00000024


Those HttpCompletion instances will be then processed by a separated thread, inside .NET thread pool…

4.     Inside .NET Thread pool

Once the request reaches at that point, it’s now being handled by .NET thread pool. Those thread pool settings were one of the first thing to check if you experience a performance issue under stress.
We use to recommend to read Contention, poor performance, and deadlocks when you make Web service requests from ASP.NET applications, that explains you how to configure settings of that thread pool like:

·         maxWorkerThreads

·         minWorkerThreads

·         maxIoThreads

·         minFreeThreads

·         minLocalRequestFreeThreads

·         maxconnection

·         executionTimeout


Seeing badly configured .NET thread pool was quite happening on a daily basis, .NET 2.0 includes now a great AutoConfig setting.
I highly recommend you to keep that autoconfig setting enabled if you don’t exactly know what you want to change and why.

From a userdump, you can easily check .NET threadpool usage using sos.dll, as lots of other blogs & sites already describe perfectly.


0:001> !threadpool

CPU utilization 12%

Worker Thread: Total: 2 Running: 1 Idle:1 MaxLimit: 200 MinLimit: 2

Work Request in Queue: 1


Number of Timers: 7


Completion Port Thread:Total: 2 Free: 2 MaxFree: 4 CurrentLimit: 2 MaxLimit: 200 MinLimit: 2


0:001> !threads

ThreadCount: 8

UnstartedThread: 0

BackgroundThread: 7

PendingThread: 0

DeadThread: 1

Hosted Runtime: no

                                      PreEmptive   GC Alloc           Lock

       ID OSID ThreadOBJ    State     GC       Context       Domain   Count APT Exception

  17    1  fb8 00115b88   1808220 Enabled  0217dac4:0217e630 000ddd40     0 Ukn (Threadpool Worker)

  21    2  974 00118708   b220 Enabled  00000000:00000000 000ddd40         0 MTA (Finalizer)

  22    3  dc8 00132ee8  80a220 Enabled  00000000:00000000 000ddd40     0 MTA (Threadpool Completion Port)

  23    4  e24 00135b30    1220 Enabled  00000000:00000000 000ddd40        0 Ukn

  24    5  f90 0014bf28   880b220 Enabled  00000000:00000000 000ddd40     0 MTA (Threadpool Completion Port)

  15    6  f6c 0014d508  880a220 Enabled  00000000:00000000 000ddd40     0 MTA (Threadpool Completion Port)

  25    7  c28 0e5e9640 180b220 Enabled  0217bd80:0217c630 000ddd40     0 MTA (Threadpool Worker)

XXXX    a   0 000cfdb0  1801820 Enabled  00000000:00000000 000ddd40     0 Ukn (Threadpool Worker)


I also recommend you to read “Production Debugging For .Net Framework Applications – Debugging Contention Problems” for detailed information regarding possible thread pool issues.

To understand correctly how .NET threading works, you might to know that .NET ThreadPoolMgr handles 5 kinds of threads:

·         Worker Thread

·         I/O Thread (aka Completion Port Thread)

·         Wait thread

·         Gate thread

·         Timer thread



a.     .NET Worker Threads

The number of .NET worker threads can be configured by maxWorkerThreads / minFreeThreads / minWorkerThreads (auto-tuned if AutoConfig setting is enabled on 2.0)
Basically a simple ASPX processing will be done in a synchronous manner by one of those worker thread (begins with mscorwks!ThreadpoolMgr::ExecuteWorkRequest()).

For example, you can see bellow a .NET worker thread that executes Page_Load() event inside my hello.aspx page:


0:028> kL999

ChildEBP RetAddr 

0619d204 698a1928 App_Web_zggy9p1v!ASP.hello_aspx.Page_Load(System.Object, System.EventArgs)+0x83

0feff2a4 6627f05f System_Web_RegularExpressions_ni!System.Web.Util.CalliHelper.EventArgFunctionCaller(….)+0x10

0feff2a4 6612bda4 System_Web_ni!System.Web.Util.CalliEventHandlerDelegateProxy.Callback(…)+0x23

0feff2a4 6612bdf0 System_Web_ni!System.Web.UI.Control.OnLoad(System.EventArgs)+0x64

0feff2a4 6613d416 System_Web_ni!System.Web.UI.Control.LoadRecursive()+0x30

0feff2a4 6613cd41 System_Web_ni!System.Web.UI.Page.ProcessRequestMain(Boolean, Boolean)+0x426

0feff2d4 6613cca7 System_Web_ni!System.Web.UI.Page.ProcessRequest(Boolean, Boolean)+0x4d

0feff310 6613cbc7 System_Web_ni!System.Web.UI.Page.ProcessRequest()+0x57

0feff32c 6613cb5a System_Web_ni!System.Web.UI.Page.ProcessRequestWithNoAssert(System.Web.HttpContext)+0x13

0feff32c 0e2d6395 System_Web_ni!System.Web.UI.Page.ProcessRequest(System.Web.HttpContext)+0x32

0feff364 65fe90df App_Web_zggy9p1v!ASP. hello _aspx.ProcessRequest(System.Web.HttpContext)+0x5

0feff364 65fba191 System_Web_ni!System.Web.HttpApplication+CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()+0x9b

0feff3a0 65fba4bb System_Web_ni!System.Web.HttpApplication.ExecuteStep(IExecutionStep, Boolean ByRef)+0x41

0feff3ec 65fb924d System_Web_ni!System.Web.HttpApplication.ResumeSteps(System.Exception)+0x163

060d8e08 65fbe244 System_Web_ni!System.Web.HttpApplication.System.Web.IHttpAsyncHandler.BeginProcessRequest(…)+0x91

0feff43c 65fbde92 System_Web_ni!System.Web.HttpRuntime.ProcessRequestInternal(System.Web.HttpWorkerRequest)+0x194

0feff470 65fbc567 System_Web_ni!System.Web.HttpRuntime.ProcessRequestNoDemand(System.Web.HttpWorkerRequest)+0x62

0feff470 79f1ef33 System_Web_ni!System.Web.Hosting.ISAPIRuntime.ProcessRequest(IntPtr, Int32)+0x57

0feff528 79f1ed6a mscorwks!COMToCLRWorkerBody+0x208

0feff584 79f3b836 mscorwks!COMToCLRWorkerDebuggerWrapper+0x37

0feff768 01cba295 mscorwks!COMToCLRWorker+0x4ba

0feff790 6a2bfe7f CLRStub[StubLinkStub]@1cba295

0feffaf0 6a2c0044 webengine!HttpCompletion::ProcessRequestInManagedCode+0x1a3

0feffafc 6a2d9475 webengine!HttpCompletion::ProcessCompletion+0x3e

0feffb10 7a110f08 webengine!CorThreadPoolWorkitemCallback+0x18

0feffb28 7a112328 mscorwks!ThreadpoolMgr::ExecuteWorkRequest+0x40

0feffb94 79ecb00b mscorwks!ThreadpoolMgr::WorkerThreadStart+0x1f2

0fefffb8 77e6608b mscorwks!Thread::intermediateThreadProc+0x49

0fefffec 00000000 kernel32!BaseThreadStart+0x34



At that point we’ve entered the normal page lifecycle done by our HttpHandler (page_init(), page_load(), page_prerender(), page_unload(), etc…)
So for a synchronous ASPX request, you can consider that your code is now being notified / executed until it finishes, and response goes to client.

b.    .NET I/O Threads (aka Completion Port Threads)

Like worker threads, the number of I/O threads is controlled by maxIoThreads / minIoThreads (auto-tuned if AutoConfig setting is enabled on 2.0).
That thread pool is bound to an I/O completion port mechanism, to handle asynchronous I/O completions that arrive either from kernel, or being reposted from usermode.

Before IIS6 WPI mode (when ASP.NET was still handled by a dedicated aspnet_wp.exe process), that I/O thread pool was being used very frequently.
At that old time, all requests were first going to inetinfo.exe process, and then forwarded to aspnet_wp.exe using an async named pipe.
Then, each request needed first to be picked up by that I/O thread, and then they got processed very frequently inside that I/O threads, or redirected to worker thread pool.

Nowadays with WPI process model, worker process directly picks up requests from kernel side http.sys by W3TP, and then directly handled by .NET worker thread pool.
However if you are using new .NET 2.0 async pages (Async=”true”) or ThreadPool.QueueUserWorkItem(), then the asynchronous part of the processing will be done inside such I/O thread.

As the following graph shows (taken the article mentioned above), an async page can be processed by 3 different threads: Worker Thread 1 -> I/O thread -> Worker Thread 2.
That means for asynchronous pages, really don’t store anything bounded to the executing thread

Synchronous vs. Asynchronous Page Processing 

Bellow is an example of a .NET I/O thread handling an Async ASP.NET 2.0 page, and currently executing EndAsyncOperation()


0:029> !tp

CPU utilization 50%

Worker Thread: Total: 2 Idle: 2 MaxLimit: 200 MinLimit: 2

Work Request in Queue: 0



Completion Port Thread: Total: 3 Free: 2 MaxFree: 4 CurrentLimit: 3 MaxLimit: 200 MinLimit: 2


0:029> !threads

ThreadCount: 9

UnstartedThread: 0

BackgroundThread: 9

PendingThread: 0

DeadThread: 0

Hosted Runtime: no

                                      PreEmptive   GC Alloc           Lock

       ID OSID ThreadOBJ    State     GC       Context       Domain   Count APT Exception

  21    2  974 00118708      b220 Enabled  02185514:02187488 000ddd40     0 MTA (Finalizer)

  22    3  dc8 00132ee8    80a220 Enabled  00000000:00000000 000ddd40     0 MTA (Threadpool Completion Port)

  23    4  e24 00135b30      1220 Enabled  00000000:00000000 000ddd40     0 Ukn

  25    7  c28 0e5e9640   180b220 Enabled  06281c70:06283828 000ddd40     0 MTA (Threadpool Worker)

  29    8  e14 000cfdb0   200b220 Enabled  0218b4c4:0218d488 0e565db8     1 MTA

   1    b  fbc 0ec4d008   880b220 Disabled 021d3448:021d3724 0e565db8     0 MTA (Threadpool Completion Port)

   4    c  264 0ec57840   880b220 Enabled  0627a544:0627b828 000ddd40     0 MTA (Threadpool Completion Port)

  13    d  e4c 0ec5a6c0   180b220 Enabled  021ce9c8:021cf724 000ddd40     0 MTA (Threadpool Worker)

  28    a  c10 0e5ea0d8   880b220 Enabled  00000000:00000000 000ddd40     0 MTA (Threadpool Completion Port)


0:001> kL999

ChildEBP RetAddr 

00a2ea38 66140a23 App_Web_bdcvaxlh!AsyncPage.EndAsyncOperation(System.IAsyncResult)+0x1d7

00a2ea64 7a5653d7 System_Web_ni!System.Web.UI.Page+PageAsyncInfo.OnAsyncHandlerCompletion(…)+0x5f

00a2ea98 7a565c72 System_ni!System.Net.LazyAsyncResult.Complete(IntPtr)+0x7f

00a2eab0 793685af System_ni!System.Net.ContextAwareResult.CompleteCallback(System.Object)+0x1a

00a2eab0 79e88f63 mscorlib_ni!System.Threading.ExecutionContext.runTryCode(System.Object)+0x43

00a2eac0 79e88ee4 mscorwks!CallDescrWorker+0x33

00a2eb40 79e88e31 mscorwks!CallDescrWorkerWithHandler+0xa3

00a2ec78 79e88d19 mscorwks!MethodDesc::CallDescr+0x19c

00a2ec90 79e88cf6 mscorwks!MethodDesc::CallTargetWorker+0x20

00a2eca4 79ef80d3 mscorwks!MethodDescCallSite::Call+0x18

00a2ee70 79ef7fde mscorwks!ExecuteCodeWithGuaranteedCleanupHelper+0xb2

00a2ef20 793684fb mscorwks!ReflectionInvocation::ExecuteCodeWithGuaranteedCleanup+0xf9

021ce844 793683ee mscorlib_ni!System.Threading.ExecutionContext.RunInternal(…)+0xa7

00000000 7a565c3b mscorlib_ni!System.Threading.ExecutionContext.Run(…)+0x92

00a2ef6c 7a5652eb System_ni!System.Net.ContextAwareResult.Complete(IntPtr)+0xa7

00a2efb8 7a57e5bd System_ni!System.Net.LazyAsyncResult.ProtectedInvokeCallback(…)+0x8b

00a2efb8 7a57e4a3 System_ni!System.Net.HttpWebRequest.ProcessResponse()+0xe1

00a2effc 7a57e2d9 System_ni!System.Net.HttpWebRequest.SetResponse(…)+0x19b

00000000 7a5aadb0 System_ni!System.Net.HttpWebRequest.SetAndOrProcessResponse(…)+0x1b1

00a2f054 7a5af3db System_ni!System.Net.ConnectionReturnResult.SetResponses(…)+0x70

00a2f094 7a5aed5f System_ni!System.Net.Connection.ReadComplete(…)+0x303

00a2f0d4 7a5aec88 System_ni!System.Net.Connection.ReadCallback(…)+0xc3

00a2f114 7a5653d7 System_ni!System.Net.Connection.ReadCallbackWrapper(…)+0x44

00a2f114 7a565bc3 System_ni!System.Net.LazyAsyncResult.Complete(…)+0x7f

00a2f12c 7a5652eb System_ni!System.Net.ContextAwareResult.Complete(…)+0x2f

00a2f174 7a60e232 System_ni!System.Net.LazyAsyncResult.ProtectedInvokeCallback(…)+0x8b

00a2f174 793d6ac4 System_ni!System.Net.Sockets.BaseOverlappedAsyncResult.CompletionPortCallback(…)+0x116

00a2f194 79e88f63 mscorlib_ni!System.Threading._IOCompletionCallback.PerformIOCompletionCallback(…)+0x68

00a2f1a8 79e88ee4 mscorwks!CallDescrWorker+0x33

00a2f228 79f20212 mscorwks!CallDescrWorkerWithHandler+0xa3

00a2f248 79f201bc mscorwks!DispatchCallBody+0x1e

00a2f2ac 79f2024b mscorwks!DispatchCallDebuggerWrapper+0x3d

00a2f2e0 7a07bebf mscorwks!DispatchCallNoEH+0x51

00a2f388 79ecb4a4 mscorwks!BindIoCompletionCallBack_Worker+0x123

00a2f398 79ecb442 mscorwks!Thread::UserResumeThread+0xfb

00a2f42c 79ecb364 mscorwks!Thread::DoADCallBack+0x355

00a2f468 79f3a0b3 mscorwks!Thread::DoADCallBack+0x541

00a2f474 7a0e17ed mscorwks!Thread::UserResumeThread+0xa6

00a2f524 7a0e27ef mscorwks!Thread::DoADCallBack+0xd9

00a2f53c 79ecb442 mscorwks!Thread::UserResumeThread+0xe1

00a2f5d0 79ecb364 mscorwks!Thread::DoADCallBack+0x355

00a2f60c 7a0e1b7e mscorwks!Thread::DoADCallBack+0x541

00a2f634 7a0e1bab mscorwks!Thread::DoADCallBack+0x575

00a2f648 7a07c031 mscorwks!ManagedThreadBase::ThreadPool+0x13

00a2f69c 7a07c063 mscorwks!BindIoCompletionCallbackStubEx+0x8c

00a2f6b0 79f2f3b0 mscorwks!BindIoCompletionCallbackStub+0x13

00a2f714 79ecb00b mscorwks!ThreadpoolMgr::CompletionPortThreadStart+0x406

00a2ffb8 77e6608b mscorwks!Thread::intermediateThreadProc+0x49

00a2ffec 00000000 kernel32!BaseThreadStart+0x34


Then depending on your application, that might be very important to have a correctly sized I/O thread pool, but once again please try first AutoConfig feature..

The 3 other kinds of threads are not directly related to a standard ASP.NET page processing, so I’ll discuss them more quickly.

c.     .NET Wait Threads

Wait threads are used to wait on a synchronization object, typically on a System.Threading.WaitHandle.
You can call ThreadPool.RegisterWaitForSingleObject() to register one of your delegate for a WaitHandle.

Bellow is what an idle wait thread looks like:


  32  Id: 230.ac0 Suspend: 0 Teb: 7ff4c000 Unfrozen

ChildEBP RetAddr  Args to Child             

1b88ff6c 7c573a4e 00000001 1b88ff84 00000000 NTDLL!ZwDelayExecution+0xb

1b88ff8c 7923558c ffffffff 00000001 198ba828 KERNEL32!SleepEx+0x32

1b88ffb4 7c57438b 00000000 198ba828 197f7498 mscorwks!ThreadpoolMgr::WaitThreadStart+0x45

1b88ffec 00000000 7923556a 1973b3d0 00000000 KERNEL32!BaseThreadStart+0x52


Maybe I’ll write later a post showing a practical sample about efficiently using wait threads in a multi threaded application.

d.    Gate Thread

The .NET gate thread is the one that is responsible for the worker threads & I/O threads creation or destruction, based on several criteria.

Bellow is what an idle gate thread looks like:


  16  Id: 930.aac Suspend: 1 Teb: 7ffaa000 Unfrozen

ChildEBP RetAddr 

01c4fe14 7c821364 ntdll!KiFastSystemCallRet

01c4fe18 77e41ea7 ntdll!NtDelayExecution+0xc

01c4fe80 79f2c9f3 kernel32!SleepEx+0x68

01c4feb4 79f2c9c3 mscorwks!EESleepEx+0xa3

01c4fee8 79f758ec mscorwks!__DangerousSwitchToThread+0x70

01c4fef4 79f2c895 mscorwks!__SwitchToThread+0xb

01c4ffb8 77e6608b mscorwks!ThreadpoolMgr::GateThreadStart+0xa1

01c4ffec 00000000 kernel32!BaseThreadStart+0x34


Check Marc Clifton very good article .NET’s ThreadPool Class – Behind The Scenes, that shows a very comprehensive graph of how ShouldGrowWorkerThread()

e.     Timer Thread

Nothing new here, timer thread is there to handle .NET System.Threading.Timer delegates…

Just one thing to say around timers, if you are still using .NET 1.1 SP1 then please ensure to have installed following fix corresponding to Q900822
FIX: When a .NET Framework based application uses the System.Threading.Timer class, the timer event may not be signaled in the .NET Framework 1.1 SP1


Ok so I think that’s now time for my 1st technical post to end after those few lines.

Again understanding how your requests will get processed is a key point to pro-actively avoid contentions under stress.
I only described there the quite hidden part that IIS & ASP.NET are doing to process a request, lots of very good articles describe how you should write your multi threaded application.


Comments (6)

  1. Basics of .NET ThreadPool class and default values

  2. The Blog entries are going to be few and far between for a few days as I am finishing content for my

  3. Saurabh Singh says:

    Hi Nico, nice to read your blog..please keep writing on stuffs related to debugging for IIS Asp.Net. I am myself a support engineer and would be regular with your blog to enhance my debugging skills.


  4. Wow, that was quite an explanation Nico!

    We are waiting for more 🙂



  5. 纹£祥 says:

    ASP.NET异步请求处理(Asynchronous Http Handlers)

  6. I’ve just finished writing up an e-mail for some new people in my team about starting Debugging and the

Skip to main content