Yeda AI Tips · #107

Español

Result And Wait Are Deadlocks Waiting

This C# habit deadlocks web apps. One .Result in the middle of an async call chain works fine on your machine, works fine in the demo, then freezes a request thread in production. Under load it's worse: many blocked threads at once starve the thread pool and the whole service stops answering. Treat every .Result and .Wait as a deadlock waiting to happen.

Why one line can freeze a request

When you await an incomplete task, the current context is captured by default and used to resume the method. In classic ASP.NET and GUI apps, that context allows only one chunk of code at a time. Now block that same context thread with .Result: the await's continuation is queued to a context the blocking thread owns, and the blocking thread won't release it until the task finishes. Each side waits for the other — a classic deadlock. Stephen Cleary calls this the single most-asked question from async newcomers: "Why does my partially async code deadlock?"

The same code runs fine in a console app (thread-pool context, no deadlock), which is exactly why it survives local testing and dies in the web app.

ASP.NET Core removed that per-request context, so the textbook deadlock is rarer — but blocking is still poison there. Microsoft's own guidance is blunt: "Many synchronous blocking calls lead to Thread Pool starvation and degraded response times." A pool sized for thousands of concurrent async requests collapses when each request pins a thread waiting on .Result.

The fix: async in, async out

Asynchrony is viral. Once one call is async, every caller above it should be async too — "efforts to be async amount to nothing unless the entire call stack is async" (David Fowler's ASP.NET Core guidance). The translation table is short:

Instead of…Use
task.Result / task.Wait()await task
Task.WaitAny(...)await Task.WhenAny(...)
Task.WaitAll(...)await Task.WhenAll(...)
Thread.Sleep(1000)await Task.Delay(1000)

Concretely:

// Deadlock (or starved thread) waiting to happen:
public IActionResult Get()
{
    var data = _service.LoadAsync().Result;   // blocks the request thread
    return Ok(data);
}

// Async all the way down:
public async Task<IActionResult> Get(CancellationToken ct)
{
    var data = await _service.LoadAsync(ct);
    return Ok(data);
}

Note the CancellationToken: .NET's cooperative cancellation model expects you to accept a token and pass it through the whole call chain, so a client that disconnects (or a timeout) can actually stop the work instead of burning a thread on a response nobody will read.

Power-user notes

Resources

Leer en español

Building an AI feature? Yeda AI designs, audits, and ships production LLM systems.

Talk to us · Read the blog