All Articles
ASP.NET Core 4 min read·

.Result Will Deadlock Your API

After years of reviewing .NET production incidents, one anti-pattern keeps showing up in the same place — blocking on async code with .Result or .Wait().

Over years of reviewing .NET production incidents, the same failure kept surfacing in the same place: an API that hangs under load for no obvious reason. The root cause is almost always the same — synchronous blocking on asynchronous code.

The pattern that bites

csharp
public IActionResult Get()
{
    // Blocking on async — the thread that owns the request
    // is now parked waiting for a continuation that needs it back.
    var data = _service.GetDataAsync().Result;
    return Ok(data);
}

Calling .Result (or .Wait()) blocks the current thread until the task completes. Under load, this ties up thread-pool threads. Each blocked request holds a thread hostage while waiting for work that itself needs a free thread to complete — and the pool drains until the API stops responding.

The task is waiting for a thread. The thread is waiting for the task. That is a deadlock — and caching or scaling won't fix it.

The fix is boring — and that's the point

csharp
public async Task<IActionResult> Get()
{
    var data = await _service.GetDataAsync();
    return Ok(data);
}

Make the method async and await the call. The thread is released back to the pool while the I/O completes, then resumes when the result is ready. Async all the way down — no .Result, no .Wait(), no GetAwaiter().GetResult() on the request path.

The rule I hold teams to: if a method calls async code, it should be async itself. The moment you block, you've traded a few characters of convenience for a scalability cliff.

#async/await#Threading#Performance#.NET
MZ

Muhammad Zain

Senior Application Engineer

Follow

Keep reading