Async Method
WebApi
IIS
ExecuteRequestHandler
Hang Issue

Async method in WebApi causes IIS ExecuteRequestHandler to hang

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

When a Web API request appears to hang in IIS at ExecuteRequestHandler, the problem is usually not that async is broken. It is usually that the code looks asynchronous at the controller boundary but blocks somewhere underneath, or that a dependency in the request path is not truly asynchronous.

The Main Cause: Sync over Async

The most common failure pattern is blocking on a Task with .Result, .Wait(), or .GetAwaiter().GetResult() inside the request pipeline. In classic ASP.NET, that can deadlock or tie up request threads badly enough that the request appears stuck in IIS.

This is the anti-pattern:

csharp
1public IHttpActionResult Get(int id)
2{
3    var order = _service.GetByIdAsync(id).Result;
4    return Ok(order);
5}

The controller is synchronous, and the asynchronous operation is forced back into blocking mode. That defeats the point of async and can cause the exact hanging behavior people see in IIS diagnostics.

Make the Entire Request Path Async

The fix is to use await from the controller downward.

csharp
1using System.Threading.Tasks;
2using System.Web.Http;
3
4public class OrdersController : ApiController
5{
6    private readonly IOrderService _service;
7
8    public OrdersController(IOrderService service)
9    {
10        _service = service;
11    }
12
13    [HttpGet]
14    [Route("api/orders/{id:int}")]
15    public async Task<IHttpActionResult> Get(int id)
16    {
17        var order = await _service.GetByIdAsync(id);
18        if (order == null)
19        {
20            return NotFound();
21        }
22
23        return Ok(order);
24    }
25}

But that alone is not enough. If GetByIdAsync internally blocks on something else, the hang can still happen.

Verify Dependencies Are Actually Async

A lot of code is “async in signature only.” For example, a service may return Task<T> but still call a synchronous database API or block on an HTTP request internally.

A correct async downstream call looks more like this:

csharp
1using System.Net.Http;
2using System.Threading.Tasks;
3
4public class ExternalClient
5{
6    private static readonly HttpClient Client = new HttpClient();
7
8    public async Task<string> FetchAsync(string url)
9    {
10        using (var response = await Client.GetAsync(url).ConfigureAwait(false))
11        {
12            response.EnsureSuccessStatusCode();
13            return await response.Content.ReadAsStringAsync().ConfigureAwait(false);
14        }
15    }
16}

If the dependency layer is synchronous, the controller's async keyword is only cosmetic.

ConfigureAwait(false) in Library Code

In classic ASP.NET code, ConfigureAwait(false) in library and infrastructure code can reduce the chance of deadlocks by avoiding unnecessary resumption on the request context.

Use it in reusable components, not usually in controller code itself.

That said, ConfigureAwait(false) is not a magical fix for blocking. If you still call .Result, you are still creating the wrong shape of code.

Check IIS Only After Fixing Code Shape

IIS settings can influence how a hang looks, but they are often secondary. Before tuning the server, verify:

  • no .Result or .Wait() in the request path
  • no fire-and-forget work that the request accidentally depends on
  • async-capable database and HTTP clients are actually being used
  • the request is not waiting on a lock or exhausted connection pool

Only after that does it make sense to look at things like request timeouts, app pool recycling, and failed-request tracing.

Instrument the Request Path

If the request still hangs, add timestamps around each awaited boundary. That tells you whether the stall is inside the controller, service, database, or an outbound HTTP call.

csharp
1using System.Diagnostics;
2
3public async Task<IHttpActionResult> Get(int id)
4{
5    var sw = Stopwatch.StartNew();
6    var order = await _service.GetByIdAsync(id);
7    sw.Stop();
8
9    Trace.WriteLine($"GetByIdAsync took {sw.ElapsedMilliseconds} ms");
10    return order == null ? (IHttpActionResult)NotFound() : Ok(order);
11}

Without timing, everything looks like “IIS hung,” even when the real bottleneck is a slow SQL query or an exhausted outbound socket pool.

Resource Starvation Can Mimic Deadlocks

A request can appear hung even when there is no logical deadlock. Examples include:

  • thread pool starvation
  • database connection pool exhaustion
  • creating a new HttpClient per request and exhausting sockets
  • long-running CPU work inside the request thread

These issues often surface as async problems because the request never completes, but the actual root cause is capacity or lifecycle management.

Common Pitfalls

A common mistake is making the controller async but keeping synchronous blocking calls in the service or repository layer.

Another mistake is using .Result inside a Web API action because it “works locally.” Local success does not prove it is safe under IIS request context and load.

Developers also often blame IIS first and application code second. In many cases, IIS is only where the stuck request becomes visible.

Finally, avoid creating a new HttpClient for each request. That can cause socket exhaustion and produce symptoms that resemble random hangs.

Summary

  • IIS hangs at ExecuteRequestHandler are often caused by sync over async blocking.
  • Use await all the way down the request path.
  • Verify that dependencies are truly asynchronous, not just wrapped in Task signatures.
  • Add timing and tracing so you can find the actual stall point.
  • Investigate resource starvation and client lifecycle issues before blaming IIS configuration alone.

Course illustration
Course illustration

All Rights Reserved.