ASP.NET Core  

Async Mistakes that can kill an ASP.NET Core application

Introduction

In this article, we will learn about 10 async mistakes that can kill an ASP.NET Core application.

Before we start, please take a look at my last article.

Let's get started.

1. Blocking on Async Code (.Result, .Wait())

Mistake

var data = httpClient.GetStringAsync(url).Result;  // ❌

Problem: This blocks the thread while waiting for the async task, potentially causing thread starvation and deadlocks under load.

Fix

var data = await httpClient.GetStringAsync(url);  // ✅

Always use await all the way down the call chain.

2. Mixing Sync and Async Code

Mistake

public IActionResult GetData()
{
    var data = GetDataAsync().Result;
    return Ok(data);
}

Problem: ASP.NET Core uses an async pipeline. Blocking calls in controllers defeats the purpose of async I/O and can freeze requests.

Fix

public async Task<IActionResult> GetData()
{
    var data = await GetDataAsync();
    return Ok(data);
}

3. Not Using ConfigureAwait(false) in Libraries

Mistake

If you write a reusable library that uses async/await, but you rely on the synchronization context:

await SomeOperationAsync();  // ❌

Problem: In ASP.NET Core it’s less critical (no SynchronizationContext), but in shared code or desktop apps, it can cause context-capturing issues.

Fix

await SomeOperationAsync().ConfigureAwait(false);  // ✅

4. Fire-and-Forget Tasks

Mistake

Task.Run(() => DoSomethingAsync());  // ❌

Problem: The task is unobserved — if it throws, the exception is lost or crashes the process.

Fix

If you must run background work:

  • Use IHostedService or BackgroundService.

  • Or handle the task safely

_ = Task.Run(async () =>
{
    try { await DoSomethingAsync(); }
    catch (Exception ex) { _logger.LogError(ex, "Background error"); }
});

5. Over-Awaiting Small Tasks (Async Overhead)

Mistake

Making everything async “just because”:

public async Task<int> AddAsync(int a, int b)
{
    return a + b; // ❌ no real async work
}

Problem: Adds overhead for no reason. Async has context-switch costs.

Fix: Keep it synchronous when no real async I/O is performed.

6. Creating Too Many HttpClient Instances

Mistake

var client = new HttpClient();
 var response = await client.GetAsync(url);

Problem: Causes socket exhaustion and memory leaks.

Fix: Use IHttpClientFactory:

public MyService(HttpClient httpClient)
{
    _httpClient = httpClient;
}

Register with:

services.AddHttpClient<MyService>();

7. Using Task.Run to “Make” Things Async

Mistake

var result = await Task.Run(() => SomeBlockingDatabaseCall());

Problem: Moves blocking code off-thread but doesn’t solve scalability — wastes thread pool threads.

Fix: Make the underlying operation truly async (e.g., EF Core’s ToListAsync())

8. Ignoring Cancellation Tokens

Mistake

public async Task ProcessRequest()
{
    await Task.Delay(5000);
}

Problem: Ignores request cancellation (like when a client disconnects).

Fix

public async Task ProcessRequest(CancellationToken token)
{
    await Task.Delay(5000, token);
}

Always respect HttpContext.RequestAborted.

9. Unobserved Task Exceptions

Mistake

_ = SomeAsyncOperation();  // ❌ exception may crash process

Problem: Exceptions in async methods not awaited can bring down your app.

Fix

Always await tasks or handle their exceptions with care.

10. Not Profiling or Measuring Async Performance

Mistake

Assuming async = faster.

Problem: Async helps scalability, not necessarily speed. Excessive async overhead can slow CPU-bound paths.

Fix: Measure using:

  • dotnet-trace

  • Application Insights

  • PerfView

  • BenchmarkDotNet

Bonus Tip: Always “async all the way”

If you start with an async method (like a controller action), propagate async through the entire call chain. Mixing sync/async is the #1 killer.

Conclusion

Here, we tried to cover async mistakes that can kill an ASP.NET Core application.